{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "0",
   "metadata": {},
   "source": [
    "# Python intro\n",
    "\n",
    "gdsfactory is written in python and requires some basic knowledge of python.\n",
    "\n",
    "If you are new to python you can find many resources online:\n",
    "\n",
    "- [books](https://jakevdp.github.io/PythonDataScienceHandbook/index.html)\n",
    "- [youTube videos](https://www.youtube.com/c/anthonywritescode)\n",
    "- [courses](https://github.com/joamatab/practical-python)\n",
    "\n",
    "This notebook is for you to experiment with some common python patterns in `gdsfactory`.\n",
    "\n",
    "## Classes\n",
    "\n",
    "Gdsfactory already has some pre-defined classes for you.\n",
    "\n",
    "All the other classes (Component, ComponentReference, Port ...) are already available in `gf.typings`\n",
    "\n",
    "Classes are good for keeping state(s), which means that they store some information inside them (polygons, ports, references ...).\n",
    "\n",
    "In gdsfactory you will write functions instead of classes. Functions are easier to write and combine, and have clearly defined inputs and outputs."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "1",
   "metadata": {},
   "outputs": [],
   "source": [
    "import gdsfactory as gf"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "c = gf.Component()\n",
    "c.add_polygon([(-8, -6), (6, 8), (7, 17), (9, 5)], layer=(1, 0))\n",
    "c.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "## Functions\n",
    "\n",
    "Functions have clear inputs and outputs, they usually accept some parameters (strings, floats, ints ...) and return other parameters.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4",
   "metadata": {},
   "outputs": [],
   "source": [
    "def double(x):\n",
    "    return 2 * x\n",
    "\n",
    "\n",
    "x = 1.5\n",
    "y = double(x)\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "It is also nice to add `type annotations` to your functions to clearly define what are the input/output types (string, int, float ...).\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "6",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "def double(x: float) -> float:\n",
    "    return 2 * x"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "## Factories\n",
    "\n",
    "A factory is a function that returns an object. In gdsfactory many functions return a `Component` object.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "def bend(radius: float = 5) -> gf.Component:\n",
    "    return gf.components.bend_euler(radius=radius)\n",
    "\n",
    "\n",
    "component = bend(radius=10)\n",
    "\n",
    "print(component)\n",
    "component.plot()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "9",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "## Decorators\n",
    "\n",
    "A decorator is a special function in Python that lets you add extra functionality to other functions, without modifying their code.\n",
    "\n",
    "In gdsfactory, decorators are used for managing functions that return a `Component`. For example, the `@cell` decorator:\n",
    "\n",
    "- Gives each `Component` a unique name based on its input parameters.\n",
    "- Caches the returned `Component` for speed and to avoid duplicating cells.\n",
    "\n",
    "For that you will see a `@cell` decorator on many component functions.\n",
    "\n",
    "An example of a decorator is `validate_call` in the pydantic library."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "10",
   "metadata": {},
   "outputs": [],
   "source": [
    "from pydantic import validate_call\n",
    "\n",
    "@validate_call\n",
    "def double(x: float) -> float:\n",
    "    return 2 * x\n",
    "\n",
    "\n",
    "x = 1.5\n",
    "y = double(x)\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "11",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "The validator decorator is equivalent to running.\n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "12",
   "metadata": {},
   "outputs": [],
   "source": [
    "def double(x: float) -> float:\n",
    "    return 2 * x\n",
    "\n",
    "\n",
    "double_with_validator = validate_call(double)\n",
    "x = 1.5\n",
    "y = double_with_validator(x)\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "13",
   "metadata": {},
   "source": [
    "Let us try to create an error `x` and you will get a clear message the the function `double` does not work with strings."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "14",
   "metadata": {},
   "source": [
    "```python\n",
    "y = double(\"not_valid_number\")\n",
    "```\n",
    "\n",
    "will raise a `ValidationError`\n",
    "\n",
    "```\n",
    "ValidationError: 1 validation error for double\n",
    "0\n",
    "  Input should be a valid number, unable to parse string as a number ...\n",
    "\n",
    "```\n",
    "\n",
    "`validate_call` will also `cast` the input type based on the type annotation. So if you pass another type it will convert it to `float`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "15",
   "metadata": {},
   "outputs": [],
   "source": [
    "x = \"12\"\n",
    "print(f\"{double(x)=}\")\n",
    "print(f\"{double_with_validator(x)=}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "16",
   "metadata": {},
   "source": [
    "## List comprehensions\n",
    "\n",
    "You will also see some list comprehensions, which are common in python.\n",
    "\n",
    "For example, you can write many loops in one line:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "17",
   "metadata": {},
   "outputs": [],
   "source": [
    "y = []\n",
    "for x in range(3):\n",
    "    y.append(double(x))\n",
    "\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "18",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "outputs": [],
   "source": [
    "y = [double(x) for x in range(3)]  # Much shorter and easier to read.\n",
    "print(y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "19",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "## Functional programming\n",
    "\n",
    "Functional programming follows the linux philosophy:\n",
    "\n",
    "- Write functions that do one thing and do it well.\n",
    "- Write functions to work together.\n",
    "- Write functions with clear **inputs** and **outputs**.\n",
    "\n",
    "### partial\n",
    "\n",
    "Partial is an easy way to modify the default arguments of a function. This is useful in gdsfactory because we define Parametric Cells (PCells) using functions. `from functools import partial`\n",
    "\n",
    "You can use a partial to create a new function with some default arguments.\n",
    "\n",
    "The following two functions are equivalent in functionality. Notice how the second one is shorter, more readable and easier to maintain thanks to `partial`.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "20",
   "metadata": {},
   "source": [
    "#### Example without partial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "21",
   "metadata": {},
   "outputs": [],
   "source": [
    "def ring_sc1(gap=0.3, **kwargs) -> gf.Component:\n",
    "    return gf.components.ring_single(gap=gap, **kwargs)\n",
    "\n",
    "# Usage\n",
    "c1 = ring_sc1(radius=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "22",
   "metadata": {},
   "source": [
    "#### Example with partial"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "23",
   "metadata": {},
   "outputs": [],
   "source": [
    "from functools import partial\n",
    "\n",
    "ring_sc2 = partial(gf.components.ring_single, gap=0.3)\n",
    "\n",
    "# Usage\n",
    "c2 = ring_sc2(radius=10)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "24",
   "metadata": {
    "lines_to_next_cell": 2
   },
   "source": [
    "`partial` becomes more useful as you customize more parameters and more widely reuse functions.\n"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "25",
   "metadata": {},
   "source": [
    "### compose\n",
    "\n",
    "`gf.compose` combines two functions into one. This is useful in gdsfactory because we\n",
    "define PCells using functions and functions are easier to combine than classes.\n",
    "\n",
    "(This is the compose function in the toolz package `from toolz import compose`)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "26",
   "metadata": {},
   "outputs": [],
   "source": [
    "ring_sc5 = partial(gf.components.ring_single, radius=10)\n",
    "add_gratings = gf.routing.add_fiber_array\n",
    "\n",
    "ring_sc_gc = gf.compose(add_gratings, ring_sc5)\n",
    "ring_sc_gc5 = ring_sc_gc(radius=5)\n",
    "ring_sc_gc5"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "27",
   "metadata": {},
   "source": [
    "This is equivalent to calling the functions one after the other."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "28",
   "metadata": {},
   "outputs": [],
   "source": [
    "ring_sc_gc5 = add_gratings(ring_sc1(radius=5))\n",
    "ring_sc_gc5"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "29",
   "metadata": {},
   "source": [
    "## Ipython\n",
    "\n",
    "This Jupyter Notebook uses an Interactive Python Terminal (Ipython). So you can interact with the code.\n",
    "\n",
    "For more details on Jupyter Notebooks, you can visit the [Jupyter website](https://jupyter.org/).\n",
    "\n",
    "The most common trick that you will see is that we use `?` to see the documentation of a function or `help(function)`."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "30",
   "metadata": {},
   "outputs": [],
   "source": [
    "gf.components.coupler?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "31",
   "metadata": {},
   "outputs": [],
   "source": [
    "help(gf.components.coupler)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "32",
   "metadata": {},
   "source": [
    "To see the source code of a function you can use `??`"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "33",
   "metadata": {},
   "outputs": [],
   "source": [
    "gf.components.coupler??"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "34",
   "metadata": {},
   "source": [
    "To time the execution time of a cell, you can add a `%time` on top of the cell."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "35",
   "metadata": {},
   "outputs": [],
   "source": [
    "%time\n",
    "\n",
    "\n",
    "def hi() -> None:\n",
    "    print(\"hi\")\n",
    "\n",
    "\n",
    "hi()"
   ]
  }
 ],
 "metadata": {
  "jupytext": {
   "cell_metadata_filter": "-all",
   "custom_cell_magics": "kql",
   "main_language": "python"
  },
  "kernelspec": {
   "display_name": "base",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.12.9"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
